W04-1. Dockerfile 과 이미지 빌드 - 강의 노트

4주차 강의 노트: Dockerfile 과 이미지 빌드 · 배포

이번 주 목표

  1. Dockerfile 의 기본 명령어(FROM, WORKDIR, COPY, RUN, ENV, ARG, USER, EXPOSE, CMD)를 읽고 쓸 수 있다.
  2. 레이어 캐시를 이해하고, 빌드가 빠른 순서로 Dockerfile 을 쓸 수 있다.
  3. 내 이미지를 Docker Hub 에 올리고(push), 남의 이미지를 받아(pull) 실행할 수 있다.
  4. Mac(arm64)과 Windows(amd64)에서 모두 도는 멀티 플랫폼 이미지를 만들 수 있다.

진행: 이론 15분 (이 노트) → 실습 45분 (실습 가이드)

지난주 복습

  • 포트 -p, 바인드 마운트 / 볼륨 -v, 사용자 정의 네트워크 --network
  • "컨테이너는 버리고 새로 만들 수 있고, 데이터는 볼륨에 둔다"


1. 이미지는 어떻게 만들어지나

지금까지는 남이 만든 이미지(nginx, postgres)를 썼습니다. 오늘은 스터디 예제 앱 "방명록(guestbook)" 을 내 이미지로 만듭니다.

flowchart TB
    S["📄 소스 코드
app.py, requirements.txt"] --> F["📝 Dockerfile
(만드는 순서를 적은 레시피)"] F -- "docker build" --> I["📦 이미지
guestbook:v1"] I -- "docker push" --> H["☁️ Docker Hub
내아이디/guestbook:v1"] H -- "docker pull / run" --> O["옆 사람 노트북
서버, Kubernetes"]
단계 명령 2주차에 본 것과 연결
레시피 작성 Dockerfile docker image history 의 CREATED BY 한 줄 한 줄이 Dockerfile 명령이었습니다
빌드 docker build -t 이름:태그 . 레시피대로 레이어를 하나씩 쌓아 이미지를 만듭니다
배포 docker push 레지스트리(Docker Hub)에 올립니다
실행 docker run 어디서든 같은 이미지로 실행합니다

2. 오늘 만드는 앱: 방명록(guestbook)

이 앱 하나를 10주차까지 계속 씁니다.

기능 이 스터디에서 쓰는 곳
이름과 한마디를 남기는 웹 페이지 (Python Flask) 4주차: 첫 이미지
DATABASE_URL 이 없으면 메모리, 있으면 PostgreSQL 에 저장 5주차 Compose, 9주차 K8s
화면에 HOSTNAME(응답한 컨테이너 이름) 표시 7~8주차 로드밸런싱 확인
APP_VERSION, APP_COLOR 로 버전과 색 표시 7주차 롤링 업데이트, 10주차 카나리 배포
/healthz, /readyz 상태 확인 주소 10주차 헬스체크(Probe)
코드를 이해할 필요는 없습니다

우리는 개발자가 아니라 이 앱을 컨테이너로 만들고 운영하는 사람의 입장입니다. Python 코드는 복사해서 쓰고, Dockerfile 에 집중합니다.

메모리 저장 모드의 함정 (실제로 겪은 일)

처음에는 웹 서버(gunicorn)를 프로세스 2개로 띄웠습니다. 그랬더니 프로세스마다 메모리가 따로라서, 새로고침할 때마다 글이 보였다 안 보였다 했습니다.
그래서 메모리 모드에서는 프로세스 1개 + 스레드 4개로 띄웁니다. "데이터를 프로세스 메모리에 두면 안 된다" 는 것을 보여 주는 좋은 예입니다. 그래서 5주차에 DB 를 붙입니다.


3. Dockerfile 한 줄씩 읽기

# ① 무엇에서 시작할지 (베이스 이미지)
FROM python:3.14-slim
# ② 작업 폴더
WORKDIR /app
# ③ 라이브러리 목록 복사 → ④ 라이브러리 설치
COPY requirements.txt .
RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt
# ⑤ 소스 코드 복사
COPY app.py .
# ⑥ 일반 사용자를 만들고, 이후는 이 사용자로 실행
RUN useradd --create-home appuser
USER appuser
# ⑦ 빌드할 때 바꿀 수 있는 값(ARG) → 실행할 때 쓰는 환경 변수(ENV)로 넘김
ARG APP_VERSION=v1
ARG APP_COLOR=royalblue
ENV APP_VERSION=${APP_VERSION} \
    APP_COLOR=${APP_COLOR}
# ⑧ 이 앱의 포트 (안내문)
EXPOSE 8000
# ⑨ 메인 프로세스
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "1", "--threads", "4", "app:app"]
Dockerfile 주석은 줄 맨 앞에만

# 주석은 줄 맨 앞에 있을 때만 주석입니다. COPY app.py . # 설명 처럼 명령 뒤에 붙이면 # 과 설명 까지 명령의 일부(복사할 파일 이름) 로 처리되어 빌드가 실패합니다.

명령 하는 일 레이어가 생기나?
FROM 시작할 이미지. 공식 이미지에서 시작하는 것이 기본 (베이스의 레이어를 그대로 씀)
WORKDIR 이후 명령이 실행될 폴더 (없으면 만듦) ✅
COPY 원본 대상 내 폴더의 파일을 이미지 안으로 복사 ✅
RUN 명령 이미지를 만드는 중에 명령 실행 (설치 등) ✅
ENV 이름=값 컨테이너 안의 환경 변수 (실행할 때 -e 로 덮어쓸 수 있음) 설정만
ARG 이름=기본값 빌드할 때만 쓰는 변수 (--build-arg 로 바꿈) 설정만
USER 이후 명령과 컨테이너를 실행할 사용자 설정만
EXPOSE 쓰는 포트를 문서로 남김 (실제로 여는 것은 -p) 설정만
CMD 컨테이너가 시작될 때 실행할 메인 프로세스 설정만
RUN 과 CMD 를 헷갈리지 마세요

  • RUN 은 이미지를 만들 때 한 번 실행됩니다 (예: 프로그램 설치).
  • CMD 는 컨테이너를 켤 때마다 실행됩니다 (예: 웹 서버 시작).

모양(exec 형식)으로 쓰세요

이렇게 쓰면 앱이 PID 1 이 되어, docker stop 이 보내는 신호(SIGTERM)를 앱이 직접 받습니다. 2주차에 본 "정중한 종료" 가 제대로 동작합니다.

왜 slim 베이스 이미지인가

태그 특징
python:3.14 빌드 도구까지 다 들어 있어 큼
python:3.14-slim 실행에 필요한 것만 남긴 Debian 기반. 대부분 이것으로 충분
python:3.14-alpine 가장 작지만, 일부 라이브러리 설치가 까다로울 수 있음

왜 USER appuser 인가 (보안 기본기)

컨테이너 안의 기본 사용자는 root(관리자) 입니다. 앱에 보안 구멍이 생겨 공격당하면 공격자가 컨테이너 안의 root 권한을 얻습니다. 일반 사용자로 실행하면 피해를 줄일 수 있습니다. 실무에서 기본으로 요구하는 항목입니다.


4. 레이어 캐시: 빌드가 빨라지는 원리

Dockerfile 의 명령 하나하나가 레이어(층) 가 됩니다. Docker 는 이전에 같은 명령으로 같은 결과를 만든 적이 있으면 그 레이어를 다시 만들지 않고 재사용(CACHED) 합니다.

규칙: 어떤 층이 바뀌면, 그 층과 그 위의 모든 층을 다시 만듭니다.

flowchart TB
    subgraph G["✅ 좋은 순서 (우리 Dockerfile)"]
        direction TB
        G1["FROM python:3.14-slim"] --> G2["COPY requirements.txt"]
        G2 --> G3["RUN pip install ← 무거움 (약 4~5초)"]
        G3 --> G4["COPY app.py ← 코드 수정 시 여기부터만"]
    end
    subgraph B["❌ 나쁜 순서"]
        direction TB
        B1["FROM python:3.14-slim"] --> B2["COPY . . ← 코드 수정 시 여기부터"]
        B2 --> B3["RUN pip install ← 매번 다시 설치!"]
    end
코드(app.py)만 고치고 다시 빌드했을 때 다시 하는 일 실측 시간
✅ 좋은 순서 COPY app.py 이후만 약 1.6초
❌ 나쁜 순서 COPY . . 이후 전부 (pip install 다시) 약 6.7초
Dockerfile 작성 요령

잘 안 바뀌는 것은 위에, 자주 바뀌는 것은 아래에.
라이브러리 목록(requirements.txt, package.json 등)을 먼저 복사해서 설치하고, 소스 코드는 맨 나중에 복사합니다.
우리 앱은 작아서 차이가 몇 초지만, 실무 앱은 몇 분 대 몇 초 차이가 납니다.

.dockerignore

docker build -t ... . 의 마지막 . 은 "이 폴더를 빌드 재료(빌드 컨텍스트)로 쓴다" 는 뜻입니다. 이미지에 들어가면 안 되는 파일(캐시, 비밀번호 파일, .git 등)은 .dockerignore 에 적어서 제외합니다. .gitignore 와 같은 방식입니다.


5. 이미지 이름 짓기와 배포

5-1. 태그 전략

태그 의미
guestbook:v1, guestbook:v2 버전마다 다른 태그. 운영에서는 항상 버전 태그를 씁니다
guestbook:latest "마지막에 올린 것" 이라는 이름표일 뿐. 운영에서는 피합니다

5-2. Docker Hub 에 올리기

Docker Hub 에 올리려면 이미지 이름이 내아이디/저장소:태그 형식이어야 합니다.

guestbook:v1                       ← 내 노트북에서만 쓰는 이름
내아이디/guestbook:v1              ← Docker Hub 에 올릴 수 있는 이름
docker.io/내아이디/guestbook:v1    ← 생략 없는 전체 이름 (2주차)
무료 계정에서 올린 이미지는 기본이 공개(Public) 입니다

누구나 받을 수 있습니다. 회사 코드, 비밀번호, 설정 파일이 들어간 이미지는 절대 공개 저장소에 올리지 마세요.
실무에서는 회사나 클라우드의 비공개 레지스트리(AWS ECR, NCR, GitHub Container Registry 등)를 씁니다.

5-3. 멀티 플랫폼 이미지: Mac 과 Windows 가 섞인 팀의 필수 지식

내 노트북 docker build 로 만든 이미지의 CPU 문제
Windows / Intel Mac amd64 Apple Silicon Mac 에서는 에뮬레이션으로 느리게 돌거나 안 될 수 있음
Apple Silicon Mac arm64 대부분의 클라우드 서버(amd64)에서 exec format error 로 실행 안 됨

해결: 두 CPU 용을 한 번에 만들어 같은 태그로 올립니다(2주차에 본 nginx 처럼).

docker buildx build --platform linux/amd64,linux/arm64 -t 내아이디/guestbook:v1 --push .
6주차부터는 이 이미지를 Kubernetes 에서 씁니다

그래서 오늘 내아이디/guestbook:v1, 내아이디/guestbook:v2 두 개를 꼭 Docker Hub 에 올려 둡니다.


핵심 요약

4주차 핵심
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
① Dockerfile   FROM → WORKDIR → COPY(목록) → RUN(설치) → COPY(코드)
                → USER(일반 사용자) → ENV/ARG → EXPOSE → CMD(메인 프로세스)
② RUN vs CMD   RUN = 빌드할 때 1번 / CMD = 컨테이너 켤 때마다
③ ARG vs ENV   ARG = 빌드할 때만 (--build-arg) / ENV = 실행할 때도 (-e 로 덮어씀)
④ 캐시         바뀐 층부터 위로 전부 다시 → 안 바뀌는 것은 위, 자주 바뀌는 것은 아래
⑤ 배포         docker build -t 이름:태그 .   →   docker push 내아이디/이름:태그
⑥ 멀티 플랫폼   docker buildx build --platform linux/amd64,linux/arm64 ... --push .
⑦ 보안         일반 사용자(USER)로 실행, 공개 저장소에 비밀 정보 금지

참고 자료

더 깊이 보고 싶다면 (볼트 내 정리 노트):

공식 문서:


네비게이션

이전 목록 다음
◀ 3주차 실습 목록으로 4주차 실습 ▶